/* 
 * Copyright 2001-2009 Terracotta, Inc. 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 
 * use this file except in compliance with the License. You may obtain a copy 
 * of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
 * License for the specific language governing permissions and limitations 
 * under the License.
 * 
 */

package cn.wapache.time.trigger;

import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cn.wapache.time.calendar.Calendar;
import cn.wapache.time.calendar.CronExpression;
import cn.wapache.time.calendar.JdkCalendar;
import cn.wapache.time.trigger.Trigger.TriggerState;

/**
 * The public interface for inspecting settings specific to a CronTrigger, .
 * which is used to fire a <code>{@link org.wapache.schedule.quartz.api.job.Job}</code>
 * at given moments in time, defined with Unix 'cron-like' schedule definitions.
 * 
 * <p>
 * For those unfamiliar with "cron", this means being able to create a firing
 * schedule such as: "At 8:00am every Monday through Friday" or "At 1:30am
 * every last Friday of the month".
 * </p>
 * 
 * <p>
 * The format of a "Cron-Expression" string is documented on the 
 * {@link org.wapache.calendar.quartz.CronExpression} class.
 * </p>
 * 
 * <p>
 * Here are some full examples: <br><table cellspacing="8">
 * <tr>
 * <th align="left">Expression</th>
 * <th align="left">&nbsp;</th>
 * <th align="left">Meaning</th>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 0 12 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 12pm (noon) every day</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * *"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am every day</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am every day</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 * * ? *"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am every day</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 * * ? 2005"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am every day during the year 2005</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 * 14 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire every minute starting at 2pm and ending at 2:59pm, every day</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 0/5 14 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 0/5 14,18 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 0-5 14 * * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire every minute starting at 2pm and ending at 2:05pm, every day</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 10,44 14 ? 3 WED"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * MON-FRI"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 15 * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on the 15th day of every month</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 L * ?"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on the last day of every month</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * 6L"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on the last Friday of every month</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * 6L"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on the last Friday of every month</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * 6L 2002-2005"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on every last Friday of every month during the years 2002, 2003, 2004 and 2005</code>
 * </td>
 * </tr>
 * <tr>
 * <td align="left"><code>"0 15 10 ? * 6#3"</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>Fire at 10:15am on the third Friday of every month</code>
 * </td>
 * </tr>
 * </table>
 * </p>
 * 
 * <p>
 * Pay attention to the effects of '?' and '*' in the day-of-week and
 * day-of-month fields!
 * </p>
 * 
 * <p>
 * <b>NOTES:</b>
 * <ul>
 * <li>Support for specifying both a day-of-week and a day-of-month value is
 * not complete (you'll need to use the '?' character in on of these fields).
 * </li>
 * <li>Be careful when setting fire times between mid-night and 1:00 AM -
 * "daylight savings" can cause a skip or a repeat depending on whether the
 * time moves back or jumps forward.</li>
 * </ul>
 * </p>
 * 
 */
public class CronTrigger extends AbstractTrigger<CronTrigger> {

	private static final long serialVersionUID = -7275292008625943826L;

    protected static final int YEAR_TO_GIVEUP_SCHEDULING_AT = CronExpression.MAX_YEAR;
    
	private static Logger logger = LoggerFactory.getLogger(CronTrigger.class);
    
    private CronExpression cronEx = null;
    private transient TimeZone timeZone = null;
    
    public CronTrigger() {
        setStartTime(System.currentTimeMillis());
        setTimeZone(TimeZone.getDefault());
    }

    public CronTrigger(String jobId){
    	this();
    	this.setId(jobId+"-cron-trigger");
    	this.setJobId(jobId);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    //
    ///////////////////////////////////////////////////////////////////////////

    public TimeZone getTimeZone(){
        if(cronEx != null) {
            return cronEx.getTimeZone();
        }
        
        if (timeZone == null) {
            timeZone = TimeZone.getDefault();
        }
        return timeZone;
    }
    public void setTimeZone(TimeZone timeZone) {
        if(cronEx != null) {
            cronEx.setTimeZone(timeZone);
        }
        this.timeZone = timeZone;
    }
    
    public String getCronExpression(){
    	return cronEx==null ? null : cronEx.getCronExpression();
    }
    public void setCronExpression(String cronExpression) throws ParseException {
        TimeZone origTz = getTimeZone();
        this.cronEx = new CronExpression(cronExpression);
        this.cronEx.setTimeZone(origTz);
    }
    /**
     * Set the CronExpression to the given one.  The TimeZone on the passed-in
     * CronExpression over-rides any that was already set on the Trigger.
     */
    public void setCronExpression(CronExpression cronExpression) {
        this.cronEx = cronExpression;
        this.timeZone = cronExpression.getTimeZone();
    }
    
    public String getExpressionSummary(){
        return cronEx == null ? null : cronEx.getExpressionSummary();
    }
    
    ///////////////////////////////////////////////////////////////////////////
    // Computation Functions
    ///////////////////////////////////////////////////////////////////////////

    protected long getTimeAfter(long afterTime) {
        return (cronEx == null) ? 0 : cronEx.getTimeAfter(afterTime);
    }

    /**
     * NOT YET IMPLEMENTED: Returns the time before the given time
     * that this <code>CronTrigger</code> will fire.
     */ 
    protected long getTimeBefore(long eTime) {
        return (cronEx == null) ? 0 : cronEx.getTimeBefore(eTime);
    }
    
	@Override
	public long computeFirstFireTime(Calendar calendar) {
        
		long next = getFireTimeAfter(startTime - 1000l);

        while (next > 0 && calendar != null && !calendar.isTimeIncluded(next)) {
        	next = calendar.getNextIncludedTime(next);
        	if(next > 0){
        		next = getFireTimeAfter(next);
        	}
        }
        
        this.nextFireTime = next;
        
        return next;
	}
	
	@Override
	public long getFireTimeAfter(long afterTime) {
        if (afterTime <=0) {
            afterTime = System.currentTimeMillis();
        }

        if (startTime > afterTime) {
            afterTime = startTime - 1000l;
        }

        if (endTime>0 && afterTime>= endTime) {
            return 0;
        }
        
        long pot = getTimeAfter(afterTime);
        if (endTime>0 && pot >0 && pot>endTime) {
            return 0;
        }

        return pot;
	}

	@Override
	public long getFinalFireTime() {
		long resultTime;
        if (endTime > 0) {
            resultTime = getTimeBefore(endTime + 1000l);
        } else {
            resultTime = (cronEx == null) ? 0 : cronEx.getFinalFireTimeInMillis();
        }
        
        if (resultTime > 0 && (startTime > 0) && resultTime < startTime) {
            return 0;
        } 
        
        return resultTime;
	}

	@Override
	public void triggered(Calendar calendar) {
		
        timesTriggered++;
        previousFireTime = nextFireTime;
        
        long next = getFireTimeAfter(nextFireTime);
        while (next > 0 && calendar != null && !calendar.isTimeIncluded(next)) {
        	next = getFireTimeAfter(next);
        }
        
        nextFireTime = next;

        // 更新状态
        updateState();
        
        logger.debug("Trigger #{} triggered, Previous={}, Next={}, Interval={}, state={}, times={}", 
        	getId(), previousFireTime, nextFireTime, (nextFireTime - previousFireTime), state, timesTriggered);
        
	}

	@Override
	public boolean updateAfterMisfire(Calendar cal) {
		
    	MisfirePolicy instr = getMisfireInstruction();

        long oldNextFireTime = this.getNextFireTime();
        long oldStartTime = this.getStartTime();
        
		switch(instr){
		case IGNORE:
			return false;
		case DO_NOTHING:
            long newFireTime = getFireTimeAfter(System.currentTimeMillis());
            while (newFireTime > 0 && cal != null && !cal.isTimeIncluded(newFireTime)) {
                newFireTime = getFireTimeAfter(newFireTime);
            }
            setNextFireTime(newFireTime);
			break;
		case SMART:
		case FIRE_ONCE_NOW:
			setNextFireTime(System.currentTimeMillis());
			break;
		case FIRE_NOW:
		case RESCHEDULE_NEXT_WITH_EXISTING_COUNT:
		case RESCHEDULE_NEXT_WITH_REMAINING_COUNT:
		case RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT:
		case RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT:
		default:
			break;
		}

		if(oldStartTime != startTime || oldNextFireTime != nextFireTime)
		{
	        Trigger.TriggerState oldState = state;
	        
	        updateState();
			
			// 有修正
	        if(logger.isDebugEnabled()){
	        	
	        	StringBuilder buf = new StringBuilder("Trigger #"+getId()+" misfire updated");

	        	if(oldState != state){
	        		buf.append(", state=").append(oldState).append("->").append(state);
	        	}
	        	if(oldStartTime != startTime){
	        		buf.append(", startTime=").append(oldStartTime).append("->").append(startTime);
	        	}
	        	if(oldNextFireTime != nextFireTime){
	        		buf.append(", nextFireTime=").append(oldNextFireTime).append("->").append(nextFireTime);
	        	}

	        	logger.debug(buf.toString());
	        }
	        
	        return true;
		}
		
		return false;
	}
		
	protected void updateState() {
		if(state == TriggerState.CREATE){
			return;
		}else if( nextFireTime < 0 ){
        	state = TriggerState.ERROR;
        }else if( nextFireTime==0 || (endTime > 0 && nextFireTime >= endTime) ){
        	state = TriggerState.COMPLETE;
        }
	}
	
	@Override
	public void updateWithNewCalendar(Calendar calendar, long misfireThreshold) {
    	
        nextFireTime = getFireTimeAfter(previousFireTime);

        if (nextFireTime <=0 || calendar == null) {
            return;
        }
        
    	this.setCalendarId(calendar.getId());
        
        long now = System.currentTimeMillis();
        while (nextFireTime >0 && !calendar.isTimeIncluded(nextFireTime)) {

            if( (nextFireTime = getFireTimeAfter(nextFireTime)) <=0) { break; }
            
            if (JdkCalendar.getYear(nextFireTime) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
            	//avoid infinite loop
                nextFireTime = -1;
            }

            if( nextFireTime > 0 && nextFireTime < now ) {
                if(now - nextFireTime >= misfireThreshold) {
                    nextFireTime = getFireTimeAfter(nextFireTime);
                }
            }
        }
	}

	@Override
	protected boolean validateMisfireInstruction(MisfirePolicy candidateMisfireInstruction) {
		switch(candidateMisfireInstruction){
		case IGNORE:
		case DO_NOTHING:
		case SMART:
		case FIRE_ONCE_NOW:
			return true;
		case FIRE_NOW:
		case RESCHEDULE_NEXT_WITH_EXISTING_COUNT:
		case RESCHEDULE_NEXT_WITH_REMAINING_COUNT:
		case RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT:
		case RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT:
		default:
			return false;
		}
	}

	@Override
	public boolean mayFireAgain() {
        return (getNextFireTime() >0);
	}
	

    /**
     * <p>
     * Determines whether the date and (optionally) time of the given Calendar 
     * instance falls on a scheduled fire-time of this trigger.
     * </p>
     * 
     * <p>
     * Equivalent to calling <code>willFireOn(cal, false)</code>.
     * </p>
     * 
     * @param test the date to compare
     * 
     * @see #willFireOn(Calendar, boolean)
     */
    public boolean willFireOn(java.util.Calendar test) {
        return willFireOn(test, false);
    }
    
    /**
     * <p>
     * Determines whether the date and (optionally) time of the given Calendar 
     * instance falls on a scheduled fire-time of this trigger.
     * </p>
     * 
     * <p>
     * Note that the value returned is NOT validated against the related
     * org.quartz.Calendar (if any)
     * </p>
     * 
     * @param test the date to compare
     * @param dayOnly if set to true, the method will only determine if the
     * trigger will fire during the day represented by the given Calendar
     * (hours, minutes and seconds will be ignored).
     * @see #willFireOn(Calendar)
     */
    public boolean willFireOn(java.util.Calendar test, boolean dayOnly) {

        test = (java.util.Calendar) test.clone();
        
        test.set(java.util.Calendar.MILLISECOND, 0); // don't compare millis.
        
        if(dayOnly) {
            test.set(java.util.Calendar.HOUR_OF_DAY, 0); 
            test.set(java.util.Calendar.MINUTE, 0); 
            test.set(java.util.Calendar.SECOND, 0); 
        }
        
        Date testTime = test.getTime();
        
        long fta = getFireTimeAfter(test.getTime().getTime() - 1000);
        
        if(fta <=0 )
            return false;

        java.util.Calendar p = java.util.Calendar.getInstance(test.getTimeZone());
        p.setTimeInMillis(fta);
        
        int year = p.get(java.util.Calendar.YEAR);
        int month = p.get(java.util.Calendar.MONTH);
        int day = p.get(java.util.Calendar.DATE);
        
        if(dayOnly) {
            return (year == test.get(java.util.Calendar.YEAR) 
                    && month == test.get(java.util.Calendar.MONTH) 
                    && day == test.get(java.util.Calendar.DATE));
        }
        
        while(fta < testTime.getTime()) {
            fta = getFireTimeAfter(fta);
        }

        return fta == testTime.getTime();
    }
    
    
    @Override
    public CronTrigger clone() {
        CronTrigger copy = (CronTrigger) super.clone();
        if (cronEx != null) {
            copy.setCronExpression(new CronExpression(cronEx));
        }
        return copy;
    }

    
}

